using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Security.Policy;
using System.Web;
using log4net;
using Microsoft.SqlServer.Types;
using SinBiota.Model.Business.MapServices.Datatypes;
using SinBiota.Model.Business.MapServices.Interfaces;

namespace SinBiota.Model.Business.MapServices.Implementation
{
    public class DrawingServices : IDrawingServices
    {
        // Bing maps: tile image has 256x256:
        private const int TileImageSize = 256;
        // required interfaces:
        private readonly ISpatialServices _spatialServices;
        private readonly IMapDatabaseServices _mapDatabaseServices;
        private readonly IHeatMapServices _heatMapServices;
        private readonly ILog _log;

        public DrawingServices(ISpatialServices spatialServices, IMapDatabaseServices mapDatabaseServices, IHeatMapServices heatMapServices)
        {
            _spatialServices = spatialServices;
            _mapDatabaseServices = mapDatabaseServices;
            _heatMapServices = heatMapServices;
            _log = LogManager.GetLogger(typeof(DrawingServices));
        }

        public Image DrawThumbnail(TileLayer tileLayer, int size)
        {
            //Create a new thumbnail image: default size if param 'size' not passed
            var image = new Bitmap(size, size, PixelFormat.Format32bppArgb);
            var graphics = Graphics.FromImage(image);
            //Anti-Aliasing?
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
            // Thumb depends on layertype:
            if (tileLayer.LayerType == LayerType.Geography)
            {
                var offset = 0;
                if (tileLayer.DrawingStyle.BoundaryColor != null)
                {
                    offset = tileLayer.DrawingStyle.BoundaryWidth;
                }
                // Graphic path around whole image:
                var points = new[] { new Point(offset, offset), new Point(size - 1 - offset, offset), new Point(size - 1 - offset, size - 1 - offset), 
                    new Point(offset, size - 1 - offset), new Point(offset, offset) };
                var graphicsPath = new GraphicsPath();
                graphicsPath.AddPolygon(points);
                // Create a region within this ring:
                var region = new Region(graphicsPath);
                //Fill:
                if (tileLayer.DrawingStyle.HasFill())
                {
                    graphics.FillRegion(tileLayer.DrawingStyle.FillBrush(), region);
                }
                //Now draw the Boundaries
                if (tileLayer.DrawingStyle.HasBoundary())
                {
                    graphics.DrawPolygon(tileLayer.DrawingStyle.BoundaryPen(), points);
                }
            }
            if (tileLayer.LayerType == LayerType.HeatMap)
            {
                //heatmap thumbnail 
                var c = size / 2 - 1;
                var heatList = new List<HeatPoint> { new HeatPoint(c, c, 150, size / 2), new HeatPoint(c, c, 120, size / 3) };
                image = _heatMapServices.CreateHeatMap(heatList, image);
            }
            if (tileLayer.LayerType == LayerType.GeographyWithIntensity)
            {
                image = (Bitmap)Image.FromFile(HttpContext.Current.Server.MapPath("~/Content/img/map/thumb.jpg"));
            }
            return image;
        }
        
        public Image DrawTileLayerImageFromGeographyList(IList<SqlGeography> geographyList, TileLayer tileLayer, string quadkey)
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            if (geographyList.Count <= 0) return null;
            // If zoom < 2, return:
            var zoomLevel = quadkey.Length;
            if (zoomLevel < 2) return null;
            //Get TileXY-Coordinates
            var tileXY = _spatialServices.QuadKeyToTileXY(quadkey);
            //Get PixelXY of the North-West Corner or the tile
            var pixelNW = TileXYToPixelXY(tileXY.X, tileXY.Y);

            //Create a new image
            var tileImage = new Bitmap(TileImageSize, TileImageSize, PixelFormat.Format32bppArgb);
            var totalPoints = 0;
            foreach (var geography in geographyList)
                totalPoints += DrawGeography(tileImage, geography, zoomLevel, pixelNW, tileLayer.DrawingStyle);
            stopwatch.Stop();
            _log.DebugFormat("Drawing quadkey {0}. Total points:{1} TIME:{2:0.00} seconds",quadkey,totalPoints,stopwatch.Elapsed.TotalSeconds);                
            return tileImage;
        }

        public Image DrawTileLayerImage(TileLayer tileLayer, string quadkey)
        {
            //Create a new image
            var tileImage = new Bitmap(TileImageSize, TileImageSize, PixelFormat.Format32bppArgb);

            // If zoom < 2, return:
            var zoomLevel = quadkey.Length;
            if (zoomLevel < 2) return null;
            //Get TileXY-Coordinates
            var tileXY = _spatialServices.QuadKeyToTileXY(quadkey);
            //Get PixelXY of the North-West Corner or the tile
            var pixelNW = TileXYToPixelXY(tileXY.X, tileXY.Y);
            //Get Latitude and Longitude of the Corners
            var nwLatLong = _spatialServices.TileXYToLatLong(tileXY.X, tileXY.Y, zoomLevel);
            var seLatLong = _spatialServices.TileXYToLatLong(tileXY.X+1, tileXY.Y+1, zoomLevel);
            var drawTime = new Stopwatch();
            // Image depends on LayerType:
            if (tileLayer.LayerType == LayerType.Geography)
            {
                var geographyList = _mapDatabaseServices.GetGeographyList(nwLatLong, seLatLong, zoomLevel, tileLayer.StoredProcedure);
                if (geographyList.Count <= 0) return null;
                drawTime.Start();
                _log.DebugFormat("DrawTileLayerImage. Layer:{0} Geographies to draw:{1}", tileLayer.Key, geographyList.Count);
                foreach (var geography in geographyList)
                    DrawGeography(tileImage, geography, zoomLevel, pixelNW, tileLayer.DrawingStyle);
                drawTime.Stop();
                _log.DebugFormat("DrawTime:{0:0.00}s",drawTime.Elapsed.TotalSeconds);
            }
            if (tileLayer.LayerType == LayerType.GeographyWithIntensity)
            {
                var geographyList = _mapDatabaseServices.GetGeographyWithIntensityList(nwLatLong, seLatLong, zoomLevel, tileLayer.StoredProcedure);
                if (geographyList.Count <= 0) return null;
                foreach (var geographyWithIntensity in geographyList)
                    DrawGeography(tileImage, geographyWithIntensity.Geography, zoomLevel, pixelNW, StyleFromIntensity(geographyWithIntensity.Intensity));
            }
            if (tileLayer.LayerType == LayerType.HeatMap)
            {
                // HEATMAP: //TODO: optimize heatmap getting only points that are close enough to be drawn;
                var heatPoints = (from mapHeatPoint in _mapDatabaseServices.GetMapHeatPoints(tileLayer)
                                  let centerPixel = _spatialServices.LatLongToPixel(mapHeatPoint.Center.Latitude, mapHeatPoint.Center.Longitude, zoomLevel, TileImageSize)
                                  let radiusPixel = _spatialServices.LatLongToPixel(mapHeatPoint.Radius + mapHeatPoint.Center.Latitude, mapHeatPoint.Radius + mapHeatPoint.Center.Longitude, zoomLevel, TileImageSize)
                                  select new HeatPoint(centerPixel.X - pixelNW.X, centerPixel.Y - pixelNW.Y, mapHeatPoint.Intensity, Math.Abs(centerPixel.X - radiusPixel.X))).ToList();
                // get heatpoints in the map and convert them to XY
                tileImage = _heatMapServices.CreateHeatMap(heatPoints, tileImage);
            }
            return tileImage;

        }

        public Image DrawLegend(TileLayer tileLayer)
        {
            const int imageHeight = 340;
            const int imageWidth = 120;
            const int legendMarginX = 20;
            const int legendMarginY = 60;
            const int legendWidth = 30;
            const int legendHeight = 256;
            const int fontSize = 10;
            const int titleFontSize = 12;
            const int legendLabelX = legendMarginX + legendWidth + fontSize; 
            var backgroundBrush = new SolidBrush(Color.LightGray);
            var borderPen = new Pen(Color.Black);
            var legendFont = new Font("Constantia", fontSize);
            var titleFont = new Font("Constantia", titleFontSize, FontStyle.Bold);

            var fontBrush = new SolidBrush(Color.Black);
            // create image and black brushes;
            var image = new Bitmap(imageWidth, imageHeight, PixelFormat.Format32bppArgb);
            var graphics = Graphics.FromImage(image);
            // draw background and border:
            graphics.FillRectangle(backgroundBrush, 0, 0, imageWidth - 1, imageHeight - 1); //image border
            graphics.DrawRectangle(borderPen, 0, 0, imageWidth - 1, imageHeight - 1); //image border
            graphics.DrawRectangle(borderPen, legendMarginX - 1, legendMarginY - 1, legendWidth + 2, legendHeight + 1); //legend border
            // Draw Scale:
            var palette = (Bitmap)Image.FromFile(HttpContext.Current.Server.MapPath("~/Content/img/map/heatpalette.jpg"));
            for (var i = 0; i < legendHeight; i++)
            {
                //get current color and draw line:
                graphics.DrawLine(new Pen(Color.FromArgb(255, palette.GetPixel(i, 0))), legendMarginX,
                                  legendMarginY + i, legendMarginX + legendWidth, legendMarginY + i);
            }
            // legend title
            var legend = tileLayer.TileLayerLegend;
            graphics.DrawString(legend.Title, titleFont, fontBrush, titleFontSize/2, titleFontSize);
            // legend scale
            var step = (legend.Maximum - legend.Minimum) / (legend.NumberOfSteps - 1);
            var value = legend.Minimum;
            for (var i = 0; i < tileLayer.TileLayerLegend.NumberOfSteps; i++)
            {
                var legendLabelY = (float)(legendHeight * ((legend.Minimum - value) / (legend.Maximum - legend.Minimum) + 1) + legendMarginY);
                graphics.DrawString(String.Format(legend.ValueFormat, value), legendFont, fontBrush, legendLabelX, legendLabelY - fontSize);
                value += step;
            }
            return image;
        }

        public void ColorImage(Bitmap bitmap, Color color) 
        {
            for (var x = 0; x < bitmap.Width; x++)
            {
                for (var y = 0; y < bitmap.Height; y++)
                {
                    var bitColor = bitmap.GetPixel(x, y);
                    //Sets all the pixels to Color but with the original alpha value
                    bitmap.SetPixel(x, y, Color.FromArgb(bitColor.A, color.R, color.G, color.B));
                }
            }
        }


        // Given H,S,L in range of 0-1
        // Returns a C# Color 
        public Color HSL2RGB(double h, double sl, double l)
        {
            double g, b;
            double r = g = b = l;
            var v = (l <= 0.5) ? (l * (1.0 + sl)) : (l + sl - l * sl);
            if (v > 0)
            {
                var m = l + l - v;
                var sv = (v - m) / v;
                h *= 6.0;
                var sextant = (int)h;
                var fract = h - sextant;
                var vsf = v * sv * fract;
                var mid1 = m + vsf;
                var mid2 = v - vsf;
                switch (sextant)
                {
                    case 0:
                        r = v;
                        g = mid1;
                        b = m;
                        break;
                    case 1:
                        r = mid2;
                        g = v;
                        b = m;
                        break;
                    case 2:
                        r = m;
                        g = v;
                        b = mid1;
                        break;
                    case 3:
                        r = m;
                        g = mid2;
                        b = v;
                        break;
                    case 4:
                        r = mid1;
                        g = m;
                        b = v;
                        break;
                    case 5:
                        r = v;
                        g = m;
                        b = mid2;
                        break;
                }
            }
            return Color.FromArgb(255, Convert.ToByte(r * 255.0f), Convert.ToByte(g * 255.0f), Convert.ToByte(b * 255.0f));
        }

        public void WriteImageToFile(Image image, string filePath, ImageFormat format = null)
        {
            if (format == null) format = ImageFormat.Png;
            var pngStream = new MemoryStream();
            image.Save(pngStream, format);
            // Create folders if necessary:
            CreateFoldersIfNeeded(filePath);
            // Create a FileStream object to write a stream to a file
            using (var fileStream = File.Create(filePath, (int)pngStream.Length))
            {
                pngStream.WriteTo(fileStream);
            }
        }

        public Image ByteArrayToImage(byte[] array)
        {
            return Image.FromStream(new MemoryStream(array));
        }

        public byte[] ImageToByteArray(Image image, ImageFormat format = null)
        {
            if (format == null) format = ImageFormat.Png;
            var  ms = new MemoryStream();
            image.Save(ms, format);
            return ms.ToArray(); 
        }

        private static void CreateFoldersIfNeeded(string filename)
        {
            var folders = filename.Split('\\');
            var checkFolderExists = folders[0];
            var i = 1;
            while (i < folders.Length)
            {
                if (!File.Exists(checkFolderExists)) Directory.CreateDirectory(checkFolderExists);
                checkFolderExists += ("\\" + folders[i++]);
            }

        }
        //return a style corresponding to the given intensity, with the same colors used in the heat map palette.
        private static GeographyDrawingStyle StyleFromIntensity(byte intensity)
        {
            var palette = (Bitmap)Image.FromFile(HttpContext.Current.Server.MapPath("~/Content/img/map/heatpalette.jpg"));
            return new GeographyDrawingStyle { FillColor = Color.FromArgb(255, palette.GetPixel(intensity, 0)), BoundaryColor = Color.LightGray, BoundaryWidth = 1 };
        }

        private int DrawGeography(Image image, SqlGeography geography, int zoomLevel, Point pixelNW, GeographyDrawingStyle style)
        {
            //Determine the number of Geometries in the object
            var numGeom = geography.STNumGeometries();
            // If there is more than 1, draw it recursively:
            //_log.DebugFormat("Num of geometries:{0}", numGeom);
            if (numGeom > 1)
            {
                var totalPoints = 0;
                for (var i = 1; i <= numGeom; i++)
                {
                    totalPoints += DrawGeography(image, geography.STGeometryN(i), zoomLevel, pixelNW, style);
                }
                return totalPoints;
            }
            // Now I know there is only 1 geography;

            // Draw geography depending of type:
            if (geography.InstanceOf("Polygon"))
            {
                return DrawPolygon(image, geography, zoomLevel, pixelNW, style);
            }
            if (geography.InstanceOf("LineString"))
            {
                return DrawLineString(image, geography, zoomLevel, pixelNW, style);
            }
            return 0;

        }

        private int DrawLineString(Image image, SqlGeography geography, int zoomLevel, Point pixelNW, GeographyDrawingStyle style)
        {
            var graphics = Graphics.FromImage(image);
            //Anti-Aliasing?
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
            if (!style.HasBoundary()) return 0;
            var linePoints = GeographyPoints(geography, zoomLevel, pixelNW);
            if (linePoints.Length < 2) return 0;
            graphics.DrawLines(style.BoundaryPen(), linePoints);
            return linePoints.Length;
            
        }

        private int DrawPolygon(Image image, SqlGeography polygon, int zoomLevel, Point pixelNW, GeographyDrawingStyle style)
        {
            var time = new Stopwatch();
            time.Start();
            var graphics = Graphics.FromImage(image);
            //Anti-Aliasing?
            graphics.SmoothingMode = SmoothingMode.AntiAlias;
            //Process the exterior Ring
            var extRingPoints = GeographyPoints(polygon.RingN(1), zoomLevel, pixelNW);
            if (extRingPoints.Length < 3) return 0;
            var totalPoints = 0;
            _log.DebugFormat("DRAW POLIGON. Exterior Ring Points:{0}", extRingPoints.Length);
            var extRingGraphicsPath = new GraphicsPath();
            extRingGraphicsPath.AddPolygon(extRingPoints);
            totalPoints += extRingPoints.Length;

            // Create a region within this ring:
            var polygonRegion = new Region(extRingGraphicsPath);

            //Process interior rings:


            var numIntRing = polygon.NumRings() - 1;
            _log.DebugFormat("Interior Rings:{0}", numIntRing);
            IList<Point[]> pointsList = new List<Point[]>();
            for (var i = 1; i <= numIntRing; i++)
            {
                // get internal ring points:
                var intRingPoints = GeographyPoints(polygon.RingN(i + 1), zoomLevel, pixelNW);
                totalPoints += intRingPoints.Length;
                // make a region with the points 
                var intRingGraphicsPath = new GraphicsPath();
                intRingGraphicsPath.AddPolygon(intRingPoints);
                var intRingRegion = new Region(intRingGraphicsPath);
                // intersect with the rectangle of the image: (IMPORTANT, otherwise it is slow if there are lots of int rings outside the image)
                intRingRegion.Intersect(new Rectangle(0, 0, TileImageSize, TileImageSize));
                // Remove this interior ring from the polygon region:
                polygonRegion.Exclude(intRingRegion);
                // Save points to draw later:
                pointsList.Add(intRingPoints);
            }
            
            if (style.HasFill())
            {
                //limit the filling area to only inside the 256x256
                polygonRegion.Intersect(new Rectangle(0, 0, TileImageSize, TileImageSize));
                // fill
                graphics.FillRegion(style.FillBrush(), polygonRegion);
            }
            
            //Now draw the Boundaries
            if (!style.HasBoundary()) return totalPoints;
            //Draw the outer ring
            var pen = style.BoundaryPen();
            graphics.DrawPolygon(pen, extRingPoints);
            //Draw the interior rings
            foreach (var points in pointsList)
            {
                graphics.DrawPolygon(pen, points);
            }
            return totalPoints;
        }

        private Point[] GeographyPoints(SqlGeography geography, int lvl, Point pixelNW)
        {
            var numPoints = (int)geography.STNumPoints();
            var pointArray = new Point[numPoints - 1];
            var maxPixel = new Point(TileImageSize, TileImageSize);
            for (var i = 1; i <= numPoints - 1; i++)
            {
                var point = geography.STPointN(i);
                var pixel = _spatialServices.LatLongToPixel((double)point.Lat, (double)point.Long, lvl, TileImageSize);
                pointArray[i - 1] = new Point(pixel.X - pixelNW.X, pixel.Y - pixelNW.Y);
                // DEBUG:
                if (pixel.X > maxPixel.X) maxPixel.X = pixel.X;
                if (pixel.Y > maxPixel.Y) maxPixel.Y = pixel.Y;

            }
            //_log.DebugFormat("MAX POINT:{0}", maxPixel);
            return pointArray;
        }

        private static Point TileXYToPixelXY(int tileX, int tileY)
        {
            return new Point(tileX * TileImageSize, tileY * TileImageSize);
        }
    }
}